/**
 * 维护聊天相关信息
 */
import { Service, getLogger, success, failed, asType, truthy } from '@gnlab/utils'
import { MemSubject, mmergeMap, mmap, type MemStream } from '@gnlab/utils/rx'
import mapValues from 'lodash/mapValues'
import { Subject } from 'rxjs'
import { type Services } from '@/services'
import {
  makeContactIdByConversationId,
  type Contact,
  ContactType,
  isWechatWxid,
} from '@/services/contacts'
import {
  type HistoryLoaderData,
  HistoryLoader,
  reloadImage,
  applyByPersonCard,
  send,
  recall,
  searchHistory,
} from './api'
import type {
  MQTTChatMessage,
  RawChatRecord,
  ChatRecord,
  RawAudioSpeechResult,
  RecvChatRecord,
  SendChatRecord,
  ContentTypeMap,
  ContactFromChat,
  ChatRecordType,
} from './definition'
import {
  parseMQTTChatRecordAndContact,
  formatRawAudioSpeechResult,
  makeSendChatRecord,
} from './definition'
import { Drafts } from './drafts'
import { InProgress } from './in-progress'
import { Metas } from './metas'

export { LoadStatus } from '@/lib/infinite-loader'
export * from './definition'
export * from './metas'
export * from './in-progress'
export { type MatchedHistory, fetchOnlineRecords } from './api'

const logger = getLogger('services/chat')

type RecvAudioRecord = RecvChatRecord<ChatRecordType.Audio>
type ImageRecord = RecvChatRecord<ChatRecordType.Image> | SendChatRecord<ChatRecordType.Image>

//  { contactId: { contact, record[] } }
type ReceivedItems = Record<string, { contact: Contact | ContactFromChat; records: ChatRecord[] }>

interface RawWarn {
  ConversationId: string
  MsgId: string
  RuleName: string
}
interface RawWarns {
  ConversationId: string
  MsgId: string
  MatchWarnRules: string[]
}
function formatWarns(raw: RawWarn | RawWarns) {
  return {
    conversationId: raw.ConversationId,
    msgId: raw.MsgId,
    rules: 'MatchWarnRules' in raw ? raw.MatchWarnRules : [raw.RuleName],
  }
}

export class Chat extends Service<Services> {
  private get communicate() {
    return this.services.mqtt.communicate
  }
  private get contacts() {
    return this.services.contacts
  }
  private get currentId() {
    return this.contacts.current.id
  }
  private get current() {
    return this.contacts.current.contact.value
  }
  private get groups() {
    return this.services.groups
  }

  metas = new Metas()
  inProgress = new InProgress()
  drafts = new Drafts()

  registered() {
    this.initListening()
    this.initAutoClearQuote()
  }

  /**
   * =========================
   * 加载聊天记录
   * =========================
   */

  /**
   * “当前联系人”的聊天记录加载器
   */
  private get historyLoader() {
    return Service.cached('Chat.historyLoader', () => {
      let currentLoader: HistoryLoader | null = null
      return this.currentId.pipe(
        mmap(contactId => {
          if (currentLoader) currentLoader.cancel()

          const contact = this.current
          if (truthy(contactId) && contact) {
            currentLoader = new HistoryLoader(contact.ownerWxid, contact.conversationId)
            void currentLoader.load() // 初始化时立即执行一次加载
            return currentLoader
          } else {
            return null
          }
        }),
      )
    })
  }

  /**
   * 当前联系人的聊天记录，没有当前联系人时为 null
   */
  get history() {
    return Service.cached('Chat.history', () => {
      return this.historyLoader.pipe(
        mmergeMap(
          loader =>
            (loader ? loader.data : new MemSubject(null)) as MemStream<HistoryLoaderData | null>,
        ),
      )
    })
  }

  /**
   * 获取指定 id 的聊天记录
   */
  private getRecordBy(condition: { msgId: string } | { receiptId: string }) {
    const filter =
      'msgId' in condition
        ? (record: ChatRecord) => record.id === condition.msgId
        : (record: ChatRecord) => record.isSend && record.receiptId === condition.receiptId

    const items = this.historyLoader.value?.data.value.items
    if (items) {
      const index = items.findIndex(filter)
      if (index !== -1) return { index, record: items[index]! }
    }
    return null
  }

  /**
   * 加载“当前联系人”的上一页聊天记录（因为聊天记录是从后往前加载，所以是上一页）
   */
  loadPrevHistory() {
    return this.historyLoader.value?.load()
  }

  /**
   * 本地添加一条聊天记录，并更新对应联系人的 latestChat
   */
  protected pushRecord(contactId: string, record: ChatRecord) {
    this.pushRecords({ [contactId]: [record] })
  }

  // items: { contactId: ChatRecord[] }
  protected pushRecords(items: Record<string, ChatRecord[]>) {
    const historyUpdates: ChatRecord[] = []
    const chatUpdates: Record<string, ChatRecord> = {}

    const historyLoader = this.historyLoader.value
    for (const [contactId, records] of Object.entries(items)) {
      // 若是来自当前会话的消息，更新聊天记录
      if (this.currentId.value === contactId) historyUpdates.push(...records)
      // 更新联系人的最近聊天信息
      chatUpdates[contactId] = records[records.length - 1]!
    }

    if (historyUpdates.length && historyLoader)
      historyLoader.updateItems([...historyLoader.data.value.items, ...historyUpdates])
    this.contacts.recordChats(chatUpdates)
  }

  /**
   * 更新指定的本地聊天记录
   */
  protected updateRecord<T extends ChatRecord>(
    contactId: string,
    msgId: string,
    updater: (record: T) => T,
  ) {
    // 非当前会话的消息，无需更新，下次加载时自然是新的
    const fromCurrentConversation = this.currentId.value === contactId
    const historyLoader = this.historyLoader.value
    if (!fromCurrentConversation || !historyLoader) return false

    const records = historyLoader.data.value.items
    const found = this.getRecordBy({ msgId })
    if (!found) {
      void logger.warn(`找不到聊天记录，无法更新。contactId：${contactId}, msgId：${msgId}`)
      return false
    }

    const updatedRecords = [...records]
    updatedRecords[found.index] = updater(found.record as T)
    historyLoader.updateItems(updatedRecords)
    return true
  }

  /**
   * 移除指定的聊天记录
   */
  protected removeRecord(contactId: string, msgId: string) {
    const fromCurrentConversation = this.currentId.value === contactId
    const historyLoader = this.historyLoader.value
    if (fromCurrentConversation && historyLoader) {
      historyLoader.updateItems(historyLoader.data.value.items.filter(v => v.id !== msgId))
    }

    // 若移除的消息就是此联系人的 latestChat，将 latestChat 置空
    const contact = this.contacts.getById(contactId)
    if (contact && contact.latestChat?.id === msgId) {
      this.contacts.recordChat(contact.id, null)
    }
  }

  /**
   * 搜索聊天记录
   */
  searchHistory = searchHistory

  /**
   * =========================
   * 接收消息
   * =========================
   */

  // 推送最新接收到的消息（这个消息可能是我们的企业微信发，也可能是我们的企业微信收。“接收”指的是通过 mqtt 渠道“接收消息推送”，而不是我们“接收”其他联系人的消息）
  private readonly _received = new Subject<ReceivedItems>()
  readonly received = this._received.asObservable()

  // 推送发现的预警信息
  private readonly _receivedWarn = new Subject<{
    contact: Contact | ContactFromChat
    msgId: string
    rules: string[]
  }>()
  readonly receivedWarn = this._receivedWarn.asObservable()

  private initListening() {
    // 所有消息都推送到 pendingMessages 里，定期统一处理
    // （不能只定期处理聊天，而让其他消息实时处理，因为其他消息依赖于聊天记录，如果没有聊天记录，其他消息会处理失败）
    setInterval(() => this.confirmMessages(), 500)

    this.communicate
      .hookWildcard<RawChatRecord>()
      .subscribe(({ message }) => this.pendingMessages.chat.push(message))

    this.communicate
      .manage<RawAudioSpeechResult>(60007)
      .subscribe(({ wxid, data }) => this.pendingMessages.speech.push({ wxid: wxid!, data }))

    // 处理与原消息分开推送的预警通知
    // 超时预警
    this.communicate
      .manage<RawWarn>(60006)
      .subscribe(({ wxid, data }) => this.pendingMessages.warn.push({ wxid: wxid!, data }))
    // 其他预警
    this.communicate
      .manage<RawWarns>(60005)
      .subscribe(({ wxid, data }) => this.pendingMessages.warn.push({ wxid: wxid!, data }))
  }

  private pendingMessages = {
    chat: [] as MQTTChatMessage[],
    speech: [] as { wxid: string; data: RawAudioSpeechResult }[],
    warn: [] as { wxid: string; data: RawWarn | RawWarns }[],
  }
  private confirmMessages() {
    const { chat, speech, warn } = this.pendingMessages
    this.pendingMessages = { chat: [], speech: [], warn: [] }

    this.confirmChatMessages(chat)
    speech.forEach(({ wxid, data }) => this.handleAudioSpeechResult(wxid, data))
    warn.forEach(({ wxid, data }) => this.handleWarn(wxid, data))
  }
  private confirmChatMessages(messages: MQTTChatMessage[]) {
    const confirmedItems: ReceivedItems = {}
    for (const message of messages) {
      if (!this.contacts.relatedWxids.value.has(message.wxid!)) {
        void logger.warn(`聊天消息所属的 wxid（${message.wxid!}）不可用，忽略此消息`, message)
        continue
      }

      const { record, contact } = parseMQTTChatRecordAndContact(message)
      if (!record) continue
      if (!contact) {
        void logger.debug('未能从聊天消息中解析出联系人信息', message)
        continue
      }

      // 若联系人未在列表里，加载其详细信息并将其加入列表
      const existContact = this.contacts.getById(contact.id)
      if (!existContact) void this.contacts.loadInfo(contact, true, record)

      // 若消息是 MQTT 回执，由回执处理方法负责处理
      const sendReceiptHandled = this.handleSendReceipt(contact, record)
      if (sendReceiptHandled) continue

      const confirmedContact = existContact ?? contact
      if (!(confirmedContact.id in confirmedItems)) {
        confirmedItems[confirmedContact.id] = { contact: confirmedContact, records: [record] }
      } else {
        confirmedItems[confirmedContact.id]!.contact = confirmedContact
        confirmedItems[confirmedContact.id]!.records.push(record)
      }
    }

    // 批量记录聊天消息
    for (const { contact, records } of Object.values(confirmedItems)) {
      for (const record of records)
        void logger.debug(`收到聊天消息（${record.type}）`, { record, contact })
    }
    this.pushRecords(mapValues(confirmedItems, item => item.records))

    // 批量推送收到消息的通知
    this._received.next(confirmedItems)

    // 标记预警信息
    for (const { contact, records } of Object.values(confirmedItems)) {
      for (const record of records) {
        if (!record.isSend && record.matchWarnRules.length) {
          this._receivedWarn.next({ contact, msgId: record.id, rules: record.matchWarnRules })
        }
      }
    }
  }

  private handleAudioSpeechResult(wxid: string, raw: RawAudioSpeechResult) {
    const result = formatRawAudioSpeechResult(raw)

    const contactId = makeContactIdByConversationId(wxid, result.conversationId)
    const contact = truthy(contactId) ? this.contacts.getById(contactId) : null
    if (!contact)
      return void logger.warn(
        `收到音频转文本内容，但找不到对应联系人。wxid: ${wxid}, conversationId: ${result.conversationId}`,
        raw,
      )
    void logger.debug('收到音频转文本内容', result)

    // 若消息来自“当前会话”，更新聊天记录
    this.updateRecord(contact.id, result.msgId, (record: RecvAudioRecord) => ({
      ...record,
      content: { ...record.content, text: result.text },
      matchWarnRules: result.matchWarnRules,
    }))

    // 标记预警信息
    if (result.matchWarnRules.length) {
      this._receivedWarn.next({ contact, msgId: result.msgId, rules: result.matchWarnRules })
    }
  }

  private handleWarn(wxid: string, raw: RawWarn | RawWarns) {
    const warn = formatWarns(raw)
    const contactId = makeContactIdByConversationId(wxid, warn.conversationId)
    const contact = truthy(contactId) ? this.contacts.getById(contactId) : null
    if (!contact)
      return void logger.warn(
        `收到预警推送，但找不到对应联系人。wxid: ${wxid}, conversationId: ${warn.conversationId}`,
        raw,
      )
    void logger.debug('收到预警推送', warn)

    // 若消息来自“当前会话”，更新聊天记录
    this.updateRecord(contact.id, warn.msgId, record => {
      if (!asType<RecvChatRecord<ChatRecordType>>(record)) return record
      return { ...record, matchWarnRules: [...record.matchWarnRules, ...warn.rules] }
    })

    // 标记预警信息
    this._receivedWarn.next({ contact, msgId: warn.msgId, rules: warn.rules })
  }

  /**
   * =========================
   * 发送消息
   * =========================
   *
   * 大体流程：
   * - 调接口执行发送，接口返回一个服务器端生成的 msgId（发送凭证 ID），本地用这个 msgId 新增一条聊天记录（接口返回不代表发送已完成）
   * - 监听 MQTT 推送的回执消息（里面会带上其对应哪条“发送凭证”），收到后，将消息标记为已发送完成
   *
   * MQTT 回执有可能比接口返回更早推送回来，若接口返回后发现凭证 ID 对应的记录已存在，则不再进行任何操作
   *
   * 消息“发送状态”变化过程：
   * 1. 初始发送状态为 Success
   * 2. 调接口后，若未报错，状态为 Success 否则为 Failed（视错误类型，也可能不再插入这条消息）
   * 3. 若 1 秒内没收到 MQTT 回执，将发送状态变为 Waiting
   * 4. 若 30 秒内都没收到 MQTT 回执，将发送状态变为 Failed
   * 5. 若收到回执（无论消息是否已标记为 Failed），都将消息重新标记为 Success
   */

  private readonly _intercepted = new Subject()
  readonly intercepted = this._intercepted.asObservable() // 消息被敏感预警拦截的通知

  /**
   * 发送消息
   * 若传入了 contactId，则发给指定联系人，否则发给当前联系人
   */
  async send(record: ChatRecord, contactId?: string): Promise<void>
  async send<T extends ChatRecordType>(
    type: T,
    content: ContentTypeMap[T],
    contactId?: string,
  ): Promise<void>
  async send<T extends ChatRecordType>(
    _type: T | ChatRecord,
    _content?: ContentTypeMap[T] | string,
    _contactId?: string,
  ) {
    const [record, contactId] =
      typeof _type === 'string'
        ? [makeSendChatRecord(_type, _content as ContentTypeMap[T]) as ChatRecord, _contactId]
        : [_type, _content as string | undefined]
    if (!record.isSend) return void logger.error('无法发送“接收”类型的消息')

    const contact = truthy(contactId) ? this.contacts.getById(contactId) : this.current
    if (!contact)
      return void logger.error(
        (truthy(contactId) ? `找不到联系人 ${contactId}` : '没有当前联系人') + '，无法发送消息',
      )

    const quoted = this.quoted.value
    const { ownerWxid, conversationId } = contact
    const request = send(ownerWxid, conversationId, record, quoted ?? undefined, () =>
      this.clearQuote(),
    )
    if (typeof request === 'string') return void logger.warn(request, record)

    const res = await request
    if (res.success) {
      // 接口未报错，更新 msgId，等待 MQTT 回执
      const receiptId = res.data.msgId
      if (this.getRecordBy({ receiptId })) return // 若绑定此凭证 ID 的消息已存在，说明回执比请求先到达了，那么无需再处理

      // 消息状态初始为“发送成功”，暂时用凭证 ID 作为消息的 ID
      this.pushRecord(contact.id, { ...record, id: receiptId, success: true })
      this.markConfirming(contact.id, receiptId)
    } else {
      // 接口报错，视为发送失败，结束流程（但若后续收到了此消息的 MQTT 推送，依然会接受，用它的结果更新消息状态）
      if (res.error === '敏感预警拦截') {
        // 被拦截的消息内容不写入聊天记录（这类消息压根不会发送出去，因此也一定不会收到 MQTT 回执）
        this._intercepted.next(undefined)
      } else {
        // 接口报出非预期的错误，将聊天消息标记为发送失败
        this.pushRecord(contact.id, { ...record, success: false })
        logger.error(`消息发送失败：${res.error}`, record)
      }
    }
  }

  // 维护“等待回执”的消息列表
  private readonly sendConfirming = new Map<string, NodeJS.Timeout>()
  private markConfirming(contactId: string, receiptId: string) {
    const timeoutId = setTimeout(() => {
      // 1 秒钟未收到回执，变为 Waiting 状态
      this.updateRecord(contactId, receiptId, _record => ({ ..._record, success: null }))

      // 30 秒未收到回执，变为 Failed 状态
      const failedTimeoutId = setTimeout(() => {
        void logger.debug(`消息发送超时 contactId:${contactId} receiptId:${receiptId}`)
        this.updateRecord(contactId, receiptId, _record => ({ ..._record, success: null }))
      }, 30 * 1000)
      this.sendConfirming.set(receiptId, failedTimeoutId)
    }, 1000)
    this.sendConfirming.set(receiptId, timeoutId)
  }
  private markSended(receiptId: string) {
    const timeoutId = this.sendConfirming.get(receiptId)
    if (timeoutId) {
      clearTimeout(timeoutId)
      this.sendConfirming.delete(receiptId)
    }
  }

  // 处理 MQTT 回执
  private handleSendReceipt(contact: ContactFromChat, record: ChatRecord) {
    if (!record.isSend || !truthy(record.receiptId) || this.currentId.value !== contact.id)
      return false
    // 若此 id 的消息已经在聊天记录里存在，说明收到的是发送消息后的 MQTT 回执，根据回执内容判断是否发送成功
    const found = this.getRecordBy({ msgId: record.receiptId })
    if (!found) return false

    const { success } = record
    void logger.debug(`消息发送${success === true ? '成功' : '失败'}（${record.type}）`, {
      record,
      contact,
    })
    this.markSended(record.receiptId)
    this.updateRecord(contact.id, record.receiptId, () => record)
    return true
  }

  /**
   * 转发消息给若干联系人
   */
  async forward(records: ChatRecord[], contacts: Contact[]) {
    // 对所有选择的目标进行转发
    for (const contact of contacts) {
      // 外部联系人有可能是搜索得来、还不存在于联系人列表，手动将其加入列表
      if (contact.type === ContactType.external && !this.contacts.getById(contact.id)) {
        this.contacts.add(contact)
      }
      for (const record of records) {
        await this.send({ ...record, isSend: true } as ChatRecord, contact.id) // eslint-disable-line no-await-in-loop
      }
    }
  }

  /**
   * =========================
   * 引用消息
   * - 标记为引用消息后，下次发送消息时会带上引用标记
   * - 切换会话、发送过一次引用消息后，会清除此标记
   * =========================
   */
  private readonly _quoted = new MemSubject<{ record: ChatRecord; nickname: string } | null>(null)
  quoted = this._quoted.asStream()

  // 切换联系人时自动清除引用标记
  private initAutoClearQuote() {
    this.currentId.subscribe(() => this.clearQuote())
  }

  // 引用消息
  quote(record: ChatRecord, nickname: string) {
    this._quoted.next({ record, nickname })
  }

  // 清除引用消息
  clearQuote() {
    if (this.quoted.value !== null) this._quoted.next(null)
  }

  /**
   * =========================
   * 其他
   * =========================
   */

  /**
   * 判断“当前联系人”的指定聊天记录是否可撤回
   */
  isRecallable(record: ChatRecord) {
    const contact = this.current
    if (!contact) return false

    if (!record.isSend || record.success !== true || record.isRecall === true) return false

    // 联系人或群是否涉及个人微信
    const isAboutWechat =
      contact.type === ContactType.group
        ? this.groups.hasWechatMember.value
        : isWechatWxid(contact.userId)
    const limit = isAboutWechat
      ? 2 * 60 * 1000 // 2分钟
      : 24 * 60 * 60 * 1000 // 24小时
    const limitTime = new Date().getTime() - limit

    return record.timestamp > limitTime
  }

  /**
   * 撤回指定消息
   */
  async recall(msgId: string) {
    const contact = this.current
    if (!contact) {
      const msg = '没有”当前联系人“，无法撤回消息'
      void logger.warn(msg)
      return failed(msg)
    }

    const res = await recall(msgId)
    if (res.success) {
      this.updateRecord(contact.id, msgId, _record => ({
        ..._record,
        success: true,
        isRecall: true,
      }))

      // 如果被撤回的是联系人的最后一条消息，销毁其最后消息的记录
      if (contact.latestChat?.id === msgId) {
        this.contacts.recordChat(contact.id, null)
      }
    }
    return res
    }

    /**
 * 重新加载置顶图片消息的图片 URL
 */
    async reloadImage(msgId: string, contactId?: string) {
        if (!truthy(contactId)) contactId = this.currentId.value ?? ''

        const res = await reloadImage(msgId)
        if (!res.success) return res

        const url = res.data
        this.updateRecord(
            contactId,
            this.currentId.value ?? '',
            (record: ImageRecord) => ({ ...record, content: { ...record.content, url } })
        )
        return success()      // 无论实际替换成功还是失败，这里都返回成功。此处的失败理由无需展示给用户，输出到日志即可
    }

  /**
   * 通过名片添加好友
   */
  readonly applyByPersonCard = applyByPersonCard
}
